HEX
Server: Apache/2.4.68 (Debian)
System: Linux as-cs-widget-demo-us-central1 6.1.0-44-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.164-1 (2026-03-09) x86_64
User: root (0)
PHP: 8.2.32
Disabled: NONE
Upload Files
File: /var/www/kevin-demo/wp-content/plugins/grocerslist/includes/Support/SalesPagePattern.php
<?php

namespace GrocersList\Support;

use GrocersList\Service\CreatorSettingsFetcher;

class SalesPagePattern
{
    public const PATTERN_NAME = 'grocerslist/sales-page';

    private CreatorSettingsFetcher $creatorSettingsFetcher;

    public function __construct(CreatorSettingsFetcher $creatorSettingsFetcher)
    {
        $this->creatorSettingsFetcher = $creatorSettingsFetcher;
    }

    public function registerPattern(): void
    {
        if (!function_exists('register_block_pattern')) {
            return;
        }

        register_block_pattern(self::PATTERN_NAME, [
            'title'       => __('GRO Sales Page', 'grocers-list'),
            'description' => __('Membership sales landing page generated by the GRO plugin.', 'grocers-list'),
            'categories'  => ['call-to-action'],
            'content'     => $this->buildContent(),
        ]);
    }

    /**
     * Build the block markup for the sales page, substituting current creator/pricing/CTA values.
     * Safe to call whether or not register_block_pattern exists — used by both registration and
     * the page-generation flow.
     *
     * The $ctaUrl override, if provided, replaces the "Join Now" href in all three CTA placements.
     * The "Login" href is always the widget login anchor.
     */
    public function buildContent(?string $ctaUrl = null): string
    {
        $joinUrl = $ctaUrl ?: $this->getJoinUrl();
        $loginUrl = $this->getLoginUrl();
        $pricingSubline = $this->getPricingSubline();

        $joinUrlEsc = esc_url($joinUrl);
        $loginUrlEsc = esc_url($loginUrl);
        $pricingSublineEsc = esc_html($pricingSubline);

        $ctaBlock = $this->buildCtaBlock($joinUrlEsc, $loginUrlEsc, $pricingSublineEsc);

        return <<<HTML
<!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull"><!-- wp:heading {"textAlign":"center","level":1} -->
<h1 class="wp-block-heading has-text-align-center">Join the Membership</h1>
<!-- /wp:heading -->

<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Get an ad-free website experience, member-only content and more exclusive perks!</p>
<!-- /wp:paragraph -->

{$ctaBlock}</div>
<!-- /wp:group -->

<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">What you get</h2>
<!-- /wp:heading -->

<!-- wp:columns -->
<div class="wp-block-columns"><!-- wp:column -->
<div class="wp-block-column"><!-- wp:heading {"textAlign":"center","level":3} -->
<h3 class="wp-block-heading has-text-align-center">An Ad-free experience</h3>
<!-- /wp:heading -->

<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Enjoy all posts on the website without ads.</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column -->

<!-- wp:column -->
<div class="wp-block-column"><!-- wp:heading {"textAlign":"center","level":3} -->
<h3 class="wp-block-heading has-text-align-center">Member exclusive content</h3>
<!-- /wp:heading -->

<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Access a growing library of content just for members.</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->

<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">What members are saying</h2>
<!-- /wp:heading -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><!-- wp:paragraph -->
<p>Blah blah quote same as before</p>
<!-- /wp:paragraph --></blockquote>
<!-- /wp:quote -->

<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">{$ctaBlock}</div>
<!-- /wp:group -->

<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">Frequently Asked Questions</h2>
<!-- /wp:heading -->

<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">How does the membership work?</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Register once using your email address and password, and you'll automatically begin your ad-free member experience.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Can I cancel anytime?</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>You can cancel any time. Stay as long as it serves you!</p>
<!-- /wp:paragraph -->

<!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull">{$ctaBlock}</div>
<!-- /wp:group -->
HTML;
    }

    private function buildCtaBlock(string $joinUrlEsc, string $loginUrlEsc, string $pricingSublineEsc): string
    {
        return <<<HTML
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
<div class="wp-block-buttons"><!-- wp:button -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="{$joinUrlEsc}">Join Now</a></div>
<!-- /wp:button -->

<!-- wp:button {"className":"is-style-outline"} -->
<div class="wp-block-button is-style-outline"><a class="wp-block-button__link wp-element-button" href="{$loginUrlEsc}">Login</a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons -->

<!-- wp:paragraph {"align":"center","fontSize":"small"} -->
<p class="has-text-align-center has-small-font-size">{$pricingSublineEsc}</p>
<!-- /wp:paragraph -->
HTML;
    }

    private function getPriceMonthly(): string
    {
        $settings = $this->creatorSettingsFetcher->getCreatorSettings();
        $price = $settings->settings->memberships->priceMonthly ?? 0;
        return $this->formatPrice($price, '$X');
    }

    private function getPriceYearly(): string
    {
        $settings = $this->creatorSettingsFetcher->getCreatorSettings();
        $price = $settings->settings->memberships->priceYearly ?? 0;
        return $this->formatPrice($price, '$Y');
    }

    private function formatPrice($price, string $fallback): string
    {
        if (!is_numeric($price) || (float) $price <= 0) {
            return $fallback;
        }

        return '$' . number_format(((float) $price) / 100, 2);
    }

    private function getPricingSubline(): string
    {
        $monthlyRaw = $this->getRawPrice('priceMonthly');
        $yearlyRaw = $this->getRawPrice('priceYearly');

        $hasMonthly = $monthlyRaw !== null;
        $hasYearly = $yearlyRaw !== null;

        if (!$hasMonthly && !$hasYearly) {
            return '$X/mo or $Y/year (Z% off)';
        }

        if ($hasMonthly && !$hasYearly) {
            return $this->formatPrice($monthlyRaw, '$X') . '/mo';
        }

        if (!$hasMonthly && $hasYearly) {
            return $this->formatPrice($yearlyRaw, '$Y') . '/year';
        }

        $monthlyTotal = $monthlyRaw * 12;
        if ($monthlyTotal <= 0 || $yearlyRaw >= $monthlyTotal) {
            return $this->formatPrice($monthlyRaw, '$X') . '/mo or ' . $this->formatPrice($yearlyRaw, '$Y') . '/year';
        }

        $discount = (int) round((($monthlyTotal - $yearlyRaw) / $monthlyTotal) * 100);
        if ($discount <= 0) {
            return $this->formatPrice($monthlyRaw, '$X') . '/mo or ' . $this->formatPrice($yearlyRaw, '$Y') . '/year';
        }

        return $this->formatPrice($monthlyRaw, '$X') . '/mo or ' . $this->formatPrice($yearlyRaw, '$Y') . '/year (' . $discount . '% off)';
    }

    private function getRawPrice(string $field): ?float
    {
        $settings = $this->creatorSettingsFetcher->getCreatorSettings();
        $price = $settings->settings->memberships->{$field} ?? null;
        if (!is_numeric($price) || (float) $price <= 0) {
            return null;
        }
        return (float) $price;
    }

    // Widget hook: BottomDrawer auto-opens the join/signup screen on hashchange.
    private function getJoinUrl(): string
    {
        return '#memberships-open';
    }

    // Widget hook: BottomDrawer opens directly to the Login screen on hashchange.
    private function getLoginUrl(): string
    {
        return '#memberships-login';
    }

    // Backwards-compat alias — pre-redesign callers / tests may still reference this name.
    private function getCheckoutUrl(): string
    {
        return $this->getJoinUrl();
    }
}